/*
 * Project:  NextGIS Mobile
 * Purpose:  Mobile GIS for Android.
 * Author:   Stanislav Petriakov, becomeglory@gmail.com
 * https://github.com/erchenger/SizeAdjustingTextView
 * *****************************************************************************
 * Copyright (c) 2015 NextGIS, info@nextgis.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.nextgis.mobile.util;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.os.Build;
import android.text.Layout.Alignment;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.SparseIntArray;
import android.util.TypedValue;
import android.widget.TextView;

/**
 * a textView that is able to self-adjust its font size depending on the min and max size of the font, and its own size.<br/>
 * code is heavily based on this StackOverflow thread:
 * http://stackoverflow.com/questions/16017165/auto-fit-textview-for-android/21851239#21851239 <br/>
 * It should work fine with most Android versions, but might have some issues on Android 3.1 - 4.04, as setTextSize will only work for the first time. <br/>
 * More info here: https://code.google.com/p/android/issues/detail?id=22493 and here in case you wish to fix it: http://stackoverflow.com/a/21851239/878126
 */
public class AutoFitTextView extends TextView
{
    private static final int NO_LINE_LIMIT=-1;
    private final RectF _availableSpaceRect=new RectF();
    private final SparseIntArray _textCachedSizes=new SparseIntArray();
    private final SizeTester _sizeTester;
    private float _maxTextSize;
    private float _spacingMult=1.0f;
    private float _spacingAdd=0.0f;
    private float _minTextSize;
    private int _widthLimit;
    private int _maxLines;
    private boolean _enableSizeCache=true;
    private boolean _initiallized=false;
    private TextPaint paint;

    private interface SizeTester
    {
        /**
         * @param suggestedSize  Size of text to be tested
         * @param availableSpace available space in which text must fit
         * @return an integer < 0 if after applying {@code suggestedSize} to
         * text, it takes less space than {@code availableSpace}, > 0
         * otherwise
         */
        public int onTestSize(int suggestedSize,RectF availableSpace);
    }

    public AutoFitTextView(final Context context)
    {
        this(context,null,0);
    }

    public AutoFitTextView(final Context context,final AttributeSet attrs)
    {
        this(context,attrs,0);
    }

    public AutoFitTextView(final Context context,final AttributeSet attrs,final int defStyle)
    {
        super(context,attrs,defStyle);
        // using the minimal recommended font size
        _minTextSize=TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,12,getResources().getDisplayMetrics());
        _maxTextSize=getTextSize();
        if(_maxLines==0)
            // no value was assigned during construction
            _maxLines=NO_LINE_LIMIT;
        // prepare size tester:
        _sizeTester=new SizeTester()
        {
            final RectF textRect=new RectF();

            @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
            @Override
            public int onTestSize(final int suggestedSize,final RectF availableSPace)
            {
                paint.setTextSize(suggestedSize);
                final String text=getText().toString();
                final boolean singleLine=getMaxLines()==1;
                if(singleLine)
                {
                    textRect.bottom=paint.getFontSpacing();
                    textRect.right=paint.measureText(text);
                }
                else
                {
                    final StaticLayout layout=new StaticLayout(text,paint,_widthLimit,Alignment.ALIGN_NORMAL,_spacingMult,_spacingAdd,true);
                    // return early if we have more lines
                    if(getMaxLines()!=NO_LINE_LIMIT&&layout.getLineCount()>getMaxLines())
                        return 1;
                    textRect.bottom=layout.getHeight();
                    int maxWidth=-1;
                    for(int i=0;i<layout.getLineCount();i++)
                        if(maxWidth<layout.getLineRight(i)-layout.getLineLeft(i))
                            maxWidth=(int)layout.getLineRight(i)-(int)layout.getLineLeft(i);
                    textRect.right=maxWidth;
                }
                textRect.offsetTo(0,0);
                if(availableSPace.contains(textRect))
                    // may be too small, don't worry we will find the best match
                    return -1;
                // else, too big
                return 1;
            }
        };
        _initiallized=true;
    }

    @Override
    public void setTypeface(final Typeface tf)
    {
        if(paint==null)
            paint=new TextPaint(getPaint());
        paint.setTypeface(tf);
        adjustTextSize();
        super.setTypeface(tf);
    }

    @Override
    public void setTextSize(final float size)
    {
        _maxTextSize=size;
        _textCachedSizes.clear();
        adjustTextSize();
    }

    @Override
    public void setMaxLines(final int maxlines)
    {
        super.setMaxLines(maxlines);
        _maxLines=maxlines;
        reAdjust();
    }

    @Override
    public int getMaxLines()
    {
        return _maxLines;
    }

    @Override
    public void setSingleLine()
    {
        super.setSingleLine();
        _maxLines=1;
        reAdjust();
    }

    @Override
    public void setSingleLine(final boolean singleLine)
    {
        super.setSingleLine(singleLine);
        if(singleLine)
            _maxLines=1;
        else _maxLines=NO_LINE_LIMIT;
        reAdjust();
    }

    @Override
    public void setLines(final int lines)
    {
        super.setLines(lines);
        _maxLines=lines;
        reAdjust();
    }

    @Override
    public void setTextSize(final int unit,final float size)
    {
        final Context c=getContext();
        Resources r;
        if(c==null)
            r=Resources.getSystem();
        else r=c.getResources();
        _maxTextSize=TypedValue.applyDimension(unit,size,r.getDisplayMetrics());
        _textCachedSizes.clear();
        adjustTextSize();
    }

    @Override
    public void setLineSpacing(final float add,final float mult)
    {
        super.setLineSpacing(add,mult);
        _spacingMult=mult;
        _spacingAdd=add;
    }

    /**
     * Set the lower text size limit and invalidate the view
     *
     * @param minTextSize
     */
    public void setMinTextSize(final float minTextSize)
    {
        _minTextSize=minTextSize;
        reAdjust();
    }

    private void reAdjust()
    {
        adjustTextSize();
    }

    private void adjustTextSize()
    {
        // This is a workaround for truncated text issue on ListView, as shown here: https://github.com/AndroidDeveloperLB/AutoFitTextView/pull/14
        // TODO think of a nicer, elegant solution.
//    post(new Runnable()
//    {
//    @Override
//    public void run()
//      {
        if(!_initiallized)
            return;
        final int startSize=(int)_minTextSize;
        final int heightLimit=getMeasuredHeight()-getCompoundPaddingBottom()-getCompoundPaddingTop();
        _widthLimit=getMeasuredWidth()-getCompoundPaddingLeft()-getCompoundPaddingRight();
        if(_widthLimit<=0)
            return;
        _availableSpaceRect.right=_widthLimit;
        _availableSpaceRect.bottom=heightLimit;
        superSetTextSize(startSize);
//      }
//    });
    }

    private void superSetTextSize(int startSize)
    {
        super.setTextSize(TypedValue.COMPLEX_UNIT_PX,efficientTextSizeSearch(startSize,(int)_maxTextSize,_sizeTester,_availableSpaceRect));
    }

    /**
     * Enables or disables size caching, enabling it will improve performance
     * where you are animating a value inside TextView. This stores the font
     * size against getText().length() Be careful though while enabling it as 0
     * takes more space than 1 on some fonts and so on.
     *
     * @param enable enable font size caching
     */
    public void setEnableSizeCache(final boolean enable)
    {
        _enableSizeCache=enable;
        _textCachedSizes.clear();
        adjustTextSize();
    }

    private int efficientTextSizeSearch(final int start,final int end,final SizeTester sizeTester,final RectF availableSpace)
    {
        if(!_enableSizeCache)
            return binarySearch(start,end,sizeTester,availableSpace);
        final String text=getText().toString();
        final int key=text==null?0:text.length();
        int size=_textCachedSizes.get(key);
        if(size!=0)
            return size;
        size=binarySearch(start,end,sizeTester,availableSpace);
        _textCachedSizes.put(key,size);
        return size;
    }

    private int binarySearch(final int start,final int end,final SizeTester sizeTester,final RectF availableSpace)
    {
        int lastBest=start;
        int lo=start;
        int hi=end-1;
        int mid=0;
        while(lo<=hi)
        {
            mid=lo+hi>>>1;
            final int midValCmp=sizeTester.onTestSize(mid,availableSpace);
            if(midValCmp<0)
            {
                lastBest=lo;
                lo=mid+1;
            }
            else if(midValCmp>0)
            {
                hi=mid-1;
                lastBest=hi;
            }
            else return mid;
        }
        // make sure to return last best
        // this is what should always be returned
        return lastBest;
    }

    @Override
    protected void onTextChanged(final CharSequence text,final int start,final int before,final int after)
    {
        super.onTextChanged(text,start,before,after);
        reAdjust();
    }

    @Override
    protected void onSizeChanged(final int width,final int height,final int oldwidth,final int oldheight)
    {
        _textCachedSizes.clear();
        super.onSizeChanged(width,height,oldwidth,oldheight);
        if(width!=oldwidth||height!=oldheight)
            reAdjust();
    }
}